/**
 * Copyright (C) 2012 Eduard Sedov
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.es.function.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.es.function.Function;
import org.es.function.FunctionRegistry;

/**
 * Because the {@linkplain FunctionRegistry} can store only one function object 
 * for each name it is not possible to register overloaded functions (Functions
 * with equal name but different parameters). This class is a wrapper for a list 
 * of such functions. 
 * <br>
 * The {@linkplain FunctionOverload} decides which function will be called for 
 * each invocation of {@link #invoke(java.lang.Object[])} method using
 * the types of parameter values.
 * 
 * @author ESEDO
 */
public class FunctionOverload implements Function {

    private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
    private Map<List<Class>, Function> functions = new LinkedHashMap();

    /**
     * Adds the given function to internal list.
     * 
     * @param function the function
     * @param argTypes the function parameter types
     */
    public void add(Function function, Class... argTypes) {
        functions.put(Arrays.asList(argTypes), function);
    }

    /**
     * Returns the internal map with all functions.
     * @return 
     */
    public Map<List<Class>, Function> getFunctions() {
        return functions;
    }

    /**
     * {@inheritDoc}
     */
    public Object invoke(Object... params) {

        if (params == null) {
            params = EMPTY_OBJECT_ARRAY;
        }

        List paramTypes = new ArrayList();
        int arguments = params.length;
        for (int i = 0; i < arguments; i++) {
            paramTypes.add(params[i].getClass());
        }

        Function function = functions.get(paramTypes);

        if (function == null) {
            for (Map.Entry<List<Class>, Function> entry : functions.entrySet()) {
                if (isAssignable(entry.getKey(), paramTypes)) {
                    function = entry.getValue();
                }
            }
        }

        if (function == null) {
            throw new IllegalArgumentException("No function found for this parameter types " + paramTypes);
        }

        return function.invoke(params);
    }

    /**
     * Tests if the given parameter types are assignable to function parameter types.
     * 
     * @param functionTypes a list of function parameter types
     * @param parameterTypes a list of current invocation parameter types
     * @return {@code true} if all {@code parameterTypes} are assignable to {@code functionTypes}
     */
    private boolean isAssignable(List<Class> functionTypes, List<Class> parameterTypes) {
        if (functionTypes.size() != parameterTypes.size()) {
            return false;
        }
        for (int i = 0; i < functionTypes.size(); i++) {
            if (!isAssignable(functionTypes.get(i), parameterTypes.get(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Tests if the given {@code parameterType} is assignable to {@code functionType}.
     * 
     * @param functionType the type of function parameters
     * @param parameterType the type of parameter value
     * @return {@code true} if {@code parameterType} is assignable to {@code functionType}
     */
    private boolean isAssignable(Class functionType, Class parameterType) {
        if (functionType.isAssignableFrom(parameterType)) {
            return true;
        }

        Class functionTypeWrapperClazz = wrappPrimitive(functionType);
        if (functionTypeWrapperClazz != null) {
            return functionTypeWrapperClazz.equals(parameterType);
        }

        return false;
    }

    /**
     * Wrapps the class of primitive type to its wrapper class.
     * @param type the class of primitive type
     * @return the wrapper class.
     */
    private Class wrappPrimitive(Class type) {
        // does anyone know a better strategy than comparing names?
        if (type.isPrimitive()) {
            if (boolean.class.equals(type)) {
                return Boolean.class;
            } else if (float.class.equals(type)) {
                return Float.class;
            } else if (long.class.equals(type)) {
                return Long.class;
            } else if (int.class.equals(type)) {
                return Integer.class;
            } else if (short.class.equals(type)) {
                return Short.class;
            } else if (byte.class.equals(type)) {
                return Byte.class;
            } else if (double.class.equals(type)) {
                return Double.class;
            } else if (char.class.equals(type)) {
                return Character.class;
            } else {
                return null;
            }
        } else {
            return type;
        }
    }
}
